//
////
//

import std.conv;
import std.file;
import std.range;
import std.stdio;

import etc.c.zlib;
import std.zlib : compress;


void main()
{
    //
    string s = "The quick brown fox jumps over the lazy dog\n";
    s ~= s;
    s ~= s;
    s ~= s;

    std.file.write("example.deflate", compress(s));

    //
    ZDecompressor zfilter;
    zfilter.init();
    scope(exit) zfilter.term();

    auto file = asInput(File("example.deflate"));
    auto decomp = filterInput(file, zfilter);

    foreach (bb; byChunk!ubyte(decomp))
        write(cast(string) bb);
}


//:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::://
// Abstract IO
//:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::://

template isBulkInput(I, T)
{
    enum bool isBulkInput = __traits(compiles,
        {
            I   input;
            T[] buffer;
            if (input.eof) {}
            size_t n = input.read(buffer);
        });
}


//----------------------------------------------------------------------------//
// Adaptor:  BulkInput --> Range
//----------------------------------------------------------------------------//

struct InputByChunk(I, T)
{
    this(I input, size_t bufferSize)
    {
        input_ = input;
        buffer_.length = bufferSize;
    }

    bool empty()
    {
        return input_.eof;
    }

    void popFront()
    {
        size_ = input_.read(buffer_);
    }

    T[] front()
    {
        if (size_ == size_.init)
        {
            assert(!input_.eof);
            popFront;
        }
        return buffer_[0 .. size_];
    }

private:
    I      input_;
    T[]    buffer_;
    size_t size_;
}

InputByChunk!(I, T) byChunk(T, I)(I input, size_t size = 512)
    if (isBulkInput!(I, T))
{
    return InputByChunk!(I, T)(input, size);
}


//----------------------------------------------------------------------------//

//
// Adaptor:  File --> Input
//
struct FileAsInput
{
    bool eof()
    {
        return file_.eof;
    }

    size_t read(ubyte[] buf)
    {
        return file_.rawRead(buf).length;
    }

private:
    File file_;
}

FileAsInput asInput(File file)
{
    return FileAsInput(file);
}


//----------------------------------------------------------------------------//

struct FilteredInput(I, F)
{
    bool eof()
    {
        return eof_ || input_.eof;
    }

    size_t read(ubyte[] buf)
    {
        ubyte[512] storage = void;
        ubyte[] srcbuf, dstbuf;

        srcbuf = storage[0 .. input_.read(storage)];
        dstbuf = buf;
        eof_ = !filter_.filter(srcbuf, dstbuf);

        return buf.length - dstbuf.length;
    }

private:
    I    input_;
    F    filter_;
    bool eof_;
}

auto filterInput(I, F)(I input, F filter)
{
    return FilteredInput!(I, F)(input, filter);
}


//:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::://
// Filter
//:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::://

struct ZDecompressor
{
    void init()
    {
        zvalidate!inflateInit(&stream_);
    }

    void term()
    {
        zvalidate!inflateEnd(&stream_);
    }

    bool filter(ref const(ubyte)[] srcbuf, ref ubyte[] dstbuf)
    {
        stream_.next_in = cast(ubyte*) srcbuf.ptr;
        stream_.avail_in = srcbuf.length;
        stream_.next_out = dstbuf.ptr;
        stream_.avail_out = dstbuf.length;

        immutable rc = zvalidate!inflate(&stream_, Z_NO_FLUSH);

        srcbuf = srcbuf[$ - stream_.avail_in  .. $];
        dstbuf = dstbuf[$ - stream_.avail_out .. $];

        end_ = (rc == Z_STREAM_END);
        return !end_;
    }

private:
    z_stream stream_;
    bool     end_;
}


//:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::://

class ZlibException : Exception
{
    /// zlib _error code
    immutable int error;

    this(int rc, string where)
    {
        error = rc;
        super(where ~ ": " ~ to!string(zError(rc)));
    }

    this(int rc)
    {
        error = rc;
        super(to!string(zError(rc)));
    }
}

/*
 * Invokes a zlib function zfunc with the arguments and validates the result
 * value.  Throws a ZlibException if the value indicates an error.
 */
private int zvalidate(alias zfunc, T...)(T args)
{
    immutable rc = zfunc(args);
    switch (rc)
    {
        case Z_OK, Z_STREAM_END:
            break;

        default:
            throw new ZlibException(rc, __traits(identifier, zfunc));
    }
    return rc;
}


